Skip to content

[SK-2195] feat(mcp): mint session tokens for a connection's MCP server - #241

Merged
ravibits merged 10 commits into
claude/inspiring-bohr-os672efrom
fix/sdk-method/mcp-connection-session-token
Oct 12, 2026
Merged

ravibits merged 10 commits into
claude/inspiring-bohr-os672efrom
fix/sdk-method/mcp-connection-session-token

Conversation

@ravibits

@ravibits ravibits commented Oct 9, 2026 •

Copy link
Copy Markdown
Contributor

Description

Every AgentKit connection has its own MCP server at <environment URL>/mcp/v3/connections/<connection name>. It serves all of that connection's tools without an MCP configuration. createSessionToken can now mint a token for that server: pass connectionName instead of mcpConfigId.

// scalekit.actions.mcp
export type CreateMcpSessionTokenParams = {
  identifier: string;
  expirySeconds?: number;
  accessLevel?: McpSessionTokenAccessLevel; // 'FULL' | 'READ_ONLY'
} & (
  | { mcpConfigId: string; connectionName?: string }
  | { connectionName: string; mcpConfigId?: undefined }
);

async createSessionToken(params: CreateMcpSessionTokenParams): Promise<CreateMcpSessionTokenResponse>

Behaviour:

  • Choosing the server:
    • mcpConfigId set to a non-empty string: a config token, built exactly as in v2.19.0. Any connectionName is ignored, as it was in v2.19.0.
    • Otherwise, connectionName set to a non-empty string: a connection token. The name is sent as the request's keyId, and mcpConfigId is never set.
    • Anything else goes to the server as in v2.19.0.
  • Connection calls only: a non-string connectionName, or an identifier that is missing, empty or blank, throws before any request.
  • Defaults: expirySeconds and accessLevel are sent only when given, so the server defaults apply for either target: 1 hour (60 seconds to 24 hours allowed), full access.
  • Access level: accessLevel: 'READ_ONLY' limits the token to tools marked read-only. Other tools are left out of tools/list, and calling one returns unknown tool "<name>".
  • Connection name: matched without regard to case, but the token is issued for the URL built from the stored name. That URL path is case-sensitive, so connect with the name exactly as stored.
  • Errors from the server:
    • ScalekitNotFoundException: no active connection with that name.
    • ScalekitBadRequestException: invalid input, a connection that isn't an AgentKit connection, or, depending on the environment's configuration, no active connected account for the identifier. Where that last case doesn't fail, the server creates a pending connected account if the identifier has none.

CreateMcpSessionTokenParams is exported from the package root. lib/ is rebuilt.

Usage

const session = await scalekit.actions.mcp.createSessionToken({
  connectionName: 'gmail',
  identifier: 'user_123',
  expirySeconds: 900,
  accessLevel: 'READ_ONLY',
});
const headers = { Authorization: `Bearer ${session.token}` };
// connect to <environment URL>/mcp/v3/connections/gmail with these headers

Testing

  • npm run build, npm run lint and npm run prettier: pass. The build leaves the committed lib/ unchanged.
  • tests/mcp-session-token.test.ts: 41 credential-free tests, all passing. They cover:
    • both call forms;
    • config calls sending the same request as v2.19.0, including missing, empty and null IDs, a stray connectionName, and { ...ctx, mcpConfigId } spreads;
    • connection-form rejections, with no request sent;
    • expirySeconds being checked first;
    • server rejections surfacing as ScalekitException subclasses;
    • compile-time checks for a call with no target or no identifier.
  • Live integration tests were not run locally; CI's live suite runs them. tests/actions.test.ts:
    • mints READ_ONLY for a connection, deleting any pending connected account the test created;
    • rejects an unknown connection with ScalekitNotFoundException;
    • confirms mcpConfigId wins when both are set.

Release

No release (skip-release). The notes wait in .changes/unreleased. They describe createSessionToken({ connectionName, identifier, expirySeconds?, accessLevel? }), say accessLevel works for either target, say mcpConfigId is used when both are set, and say existing mcpConfigId calls are unchanged.

Scope

🤖 Generated with Claude Code

saif-at-scalekit and others added 7 commits October 9, 2026 07:08
* chore(release): release tool, release.toml and seeded CHANGELOG

Vendored from scalekit-sdks-wrapper/tools/release. The matching
workflow changes (release-checks.yml, release.yml, build.yml
workflow_call) ship in a separate commit because they need a push
credential with the GitHub 'workflow' scope.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017ANmBCXNdijNmAJ49zFRPv

* chore(release): drive releases from scalekit-sdks-wrapper

The release tool and its config now live only in the wrapper
(tools/release). This repo keeps CHANGELOG.md and .changes/unreleased/
(release notes waiting for the next release).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017ANmBCXNdijNmAJ49zFRPv

* ci(release): approval-gated release workflow

- release.yml (workflow_dispatch, version input): validates the version
  against the source, the tags and CHANGELOG.md; reuses build.yml as CI;
  builds; waits for the 'release' environment approval; tags with the
  release app; publishes; creates the GitHub Release from the CHANGELOG;
  verifies the registry. dry_run rehearses without tagging or publishing.
- build.yml: add workflow_call so the release reuses the exact PR CI.

Generated by scalekit-sdks-wrapper/tools/release/templates/workflows/generate.py.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017ANmBCXNdijNmAJ49zFRPv

* docs: describe how this repo's existing release workflow is started

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017ANmBCXNdijNmAJ49zFRPv

* ci(release): keep the existing release workflow

Restores .github/workflows as on main: releases keep using the existing
workflow (or stay manual), started from scalekit-sdks-wrapper after
release.py preflight; every PR already has to pass build.yml.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017ANmBCXNdijNmAJ49zFRPv

* docs: describe release notes and the release workflow

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017ANmBCXNdijNmAJ49zFRPv

* docs(changelog): add the latest release and simplify the header

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(changelog): fix typos and defaults in imported release history

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(changelog): drop leftover contributor handles, reword the import note

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(changelog): fix typos and a broken reference in imported release history

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
Add actions.mcp.createConnectionSessionToken({ connectionName, identifier,
expirySeconds?, accessLevel? }). It mints a session token for the MCP
server of a single connection (/mcp/v3/connections/<connectionName>) by
sending the connection name as keyId and leaving mcpConfigId unset. The
expirySeconds validation is shared with createSessionToken, whose
behaviour is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…unt rules

Document that connectionName is matched to the stored name's casing for
the token, the accepted expiry range (60 seconds to 24 hours, default
1 hour), and that a missing connected account is either rejected or
results in a pending account, depending on the environment.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…tokens

The connection name is matched without regard to case and the token is
issued for the server URL built from the connection's stored name. Only
that URL is case-sensitive, so callers connect using the stored name.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
State that identifier is 1 to 255 characters, and tighten the release
note for createConnectionSessionToken.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Oct 9, 2026 •

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration
  • Configuration used: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: c66c3d56-cf1e-4651-9e0d-58d90ed9b63b

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Comment @coderabbitai help to get the list of available commands.

claude added 3 commits October 9, 2026 20:21
createSessionToken now accepts either mcpConfigId or connectionName.
The params type is a union so TypeScript rejects both and neither, and
the method also checks at runtime that exactly one non-empty target is
set before sending a request. A connectionName is sent as keyId; calls
with mcpConfigId are unchanged. The separate connection method is
removed, and the reference and release note describe the new form.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
createSessionToken validates expirySeconds first, as before, and only
checks the target for the connectionName form. Without a non-empty
connectionName the request is built exactly as in v2.19.0: mcpConfigId
is passed through as given, an empty connectionName is ignored, and a
missing ID still reaches the server. A connectionName alongside
mcpConfigId, or a non-string connectionName, throws before any request.

Tests pin the unchanged mcpConfigId behaviour, and the live connection
test deletes a connected account only if it created one. The reference
and release note say the new checks apply to connectionName only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A call that sets both mcpConfigId and connectionName, for example by
spreading a context object that carries connectionName, compiled and
minted a configuration token before. It does again: a non-empty
mcpConfigId always selects the configuration and the request is built
as before. connectionName is used only when mcpConfigId is missing or
empty, and only that form is checked before the request, now including
an empty identifier ("identifier is required").

The params type lets the configuration form carry connectionName and
still rejects a call with neither target or without an identifier. The
reference and release note describe the precedence and the token
lifetime range for both targets.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ravibits
ravibits changed the base branch from main to claude/inspiring-bohr-os672e October 9, 2026 21:07

Copy link
Copy Markdown
Contributor Author

When merging: several commits on this branch end with a Claude-Session: trailer. This repository is public, so please delete those trailer lines from the squash-commit message before you confirm the merge. Keep the Co-Authored-By lines.


Generated by Claude Code

@saif-at-scalekit saif-at-scalekit left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tried minting a session token for a connection’s own MCP server with connectionName + identifier, then pointing an MCP client at that connection URL.

After this ships, that path works the same way the Virtual MCP config path already did — nothing else to request for the developer experience.

Sent by pocket agent saif-waiting-review

@ravibits
ravibits merged commit 3327201 into claude/inspiring-bohr-os672e Oct 12, 2026
4 checks passed
@ravibits
ravibits deleted the fix/sdk-method/mcp-connection-session-token branch October 12, 2026 06:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants